Binary Indexed Tree 树状数组
更新日期:
Binary Indexed Tree 树状数组
做Leetcode 做到MeetingRoomII的时候我知道不用线段树或者树状数组是不太好搞了。还是来学习一下吧。
树状数组算是线段树的一种特殊情况(子集),所以树状数组能解决的问题线段树一定能做,但线段树能做的树状数组不一定能做。
引入的问题
问题1
对一个数组进行如下操作
update(i1,i2,operation)
求 value(i)=?
例如:int[] ar=new int[N]
update(ar,2,5,+,4) //(2 to 5).map(ar(_)+=4)
return ar(k)
问题2
对一个数组进行如下操作 update(index,operation,value)
求 sum(i1,i2)=?
例如:int[] ar=new int[N]
update(ar,3,+,5); //ar[3]+=5
update(ar,11,-,3); //ar[11]-=3
。。。。。。
求:sum(ar,0,12)? //(0 to 12).map(ar(_)).sum
应用线段树的思路,对问题1,可以在中间节点缓存操作O(lgn),求解时再计算出结果。
对问题2,可以直接缓存sum,每次操作时都对sum附带进行操作O(lgn),而求结果时,直接使用缓存的sum结果O(lgn)。
树状数组的解法
按照刚才的解题分析,应当使用线段树进行操作,但线段树操作效率比较低,是否一种折中的办法呢?
例如:把线段树的每个节点映射到一个额外的数组上?
那么问题很明确,如何将一颗有N个叶子节点的树映射到一个长度为N的数组上?
最直观的思路显然是这样:
图看上去很容易理解,我们希望将中间结果(1~2)(5~8)等存在另外一个数组B[]里,剩下的问题只有一个,怎么把这些节点向数组的index映射?而且这个映射显然是算法可描述的,这样在计算时才能容易找到各个节点。
究竟怎样想出来的这种映射方式已经不得而知,但的确是个很神妙的构想,这也是Binary Index Tree的精髓了。
基本设计
图2中黄色的块被废弃了,如果用a->b表示a存入b则:
(1~2)->2
(1~4)->4
(5~6)->6
(1~8)->8
1->1
3->3
5->5
7->7
是不是有点规律了?
- 奇数点全部存原数组值
- 偶书点K存入的位数与K&(-K)后面的0相关,由M个0就存了1<<M个数字
求解问题2
如果希望计算(st,ed)的sum时,如何计算呢?直接计算st到ed之间的数据相当难算,但是
1 | sum(st,ed)=sum(1,ed)-sum(1,st-1) |
这时候再看一下图2是不是明白了?
从新定义一个函数sumFromStart(k)表示从1加到K的和。
1 | sum(st,ed)=sumFromStart(ed)-sumFromStart(st-1) |
最后看看这个sumFromStart写法吧其实很容易想到:
- 2 是10计算1~2的和
- 4 是100计算了1~4的和
- 8 是1000计算了1~8的和
如果我们想算1~7
7=111=100+10+1
而且1~7=(1~4)+(5~6)+7
所以sumFromStart(7)=B(4)+B(6)+B(7)
再写清楚点,如果是二进制:
sumFromStart(111)=B(100)+B(110)+B(111)
想明白了? 还没有?那就看图吧
所以,sumFromStart(k)定义如下
1 | int sumFromStart(int k,int[] b){ |
最后还有一个更新操作,因为是单个更新,所以注意要把上面的点也更新了,以对A[5],操作为例,需要更新B(5),B(6),B(8)。写出来这三个
B(101)
B(110)
B(1000)
看不出啥太明显的规律啊?
1 | 101 +1 =110 |
明白了么? k+k&(-k)啊,所以update写出来
1 | void update(int index,int v,Operation=add,int[] b){ |
求解问题1
问题1是段累加,单点求值,所以可以把段累加过程加入B数组(复杂度lgn),求解时再算单点(复杂度lgn)
1 | int[] b=new int[N]; |
求解MeetingRoomII
最后给出meeting RoomII代码,很遗憾,这个题最后求最大重叠,所以遍历了每个点找最大,时间复杂度O(nlgn)
1 | public int minMeetingRooms(Interval[] ins) { |